/**
 * ISSUE:
 *      1. rwlock is better here
 *      2. not check the multi-mount or umount
 */

#include "device.h"
#include "assert.h"
#include "mstring.h"
#include "general.h"
#include "driver.h"
#include "matrix.h"

/* base device pointer */
mx_carray_t ndevbase;   // not matched device base
mx_carray_t ddevbase;   // direct device base

/* extern from driver.c */
extern mx_carray_t drvbase;
extern mx_mutex_t glbmutex;

/**
 * mount a device to system
 * @param pdev: the descriptor of device
 * @param data: the information of device
 * @param ops: the operations of device, the device will work on direct mode if it is not null
 * @return: void
 */
void mx_device_mount (mx_device_pt pdev, mx_device_data_pt data, mx_device_ops_pt ops)
{
    assert_false(pdev == mx_null);
    assert_false(data == mx_null);

    /* initialize attributes */
    pdev->isopen = mx_false;
    pdev->oc = 0;
    pdev->seek == 0;
    pdev->data = data;
    pdev->rtdata = mx_null;
    if (ops != mx_null) // mark the device is direct and no need driver
    {
        pdev->isdirect = mx_true;
        pdev->rely.ops = ops;
    }
    else
    {
        pdev->isdirect = mx_false;
        pdev->rely.drv = mx_null;
    }
    mx_mutex_init(&pdev->mutex);
    mx_carray_node_init(&pdev->node);

    mx_mutex_enter(&glbmutex);

    if (pdev->isdirect)
    {
        mx_carray_append(&ddevbase, &pdev->node);
    }
    else
    {
        mx_bool_t res = mx_false;
        mx_driver_pt pdrv;
        /* match driver */
        mx_carray_foreach(&drvbase, n, {
            pdrv = container(n, mx_driver_t, node);
            if (mx_strcmp(pdrv->name, data->drvname) == 0)
            {
                /* call the event of driver */
                res = pdrv->events->onlinkup(pdrv, pdev);
                break;
            }
        });

        /* match driver successful and insert the device to driver's device chain */
        if (res)
        {
            pdev->rely.drv = (mx_void_pt)pdrv;
            mx_carray_append(&pdrv->devbase, &pdev->node);
        }
        else
        {
            mx_carray_append(&ndevbase, &pdev->node);
        }

    }

    mx_mutex_leave(&glbmutex);
}

/**
 * umount a device from system
 * @param pdev: the descriptor of device
 * @return: the result of umount operation (mx_err_t)
 */
mx_err_t mx_device_umount (mx_device_pt pdev)
{
    assert_false(pdev == mx_null);

    mx_err_t ret = EOK;

    /* check whether the device is in use */
    mx_bool_t res = mx_mutex_tryenter(&pdev->mutex);
    if (res && !pdev->isopen)   // not in use (not opened)
    {
        mx_mutex_enter(&glbmutex);

        /* call the event of driver */
        if (pdev->rely.drv != mx_null)
        {
            ((mx_driver_pt)pdev->rely.drv)->events->onlinkdown(((mx_driver_pt)pdev->rely.drv), pdev);
        }
        pdev->rely.drv = mx_null;

        /* remove from device chain */
        mx_carray_remove(&pdev->node);

        mx_mutex_leave(&glbmutex);
    }
    else
    {
        ret = EDEV_BUSY;
    }

    mx_mutex_leave(&pdev->mutex);

    return ret;
}

/**
 * get the count of device in system
 * @param: void
 * @return: device counts (mx_usize_t)
 */
mx_usize_t mx_device_count ()
{
    mx_usize_t ret = 0;
    
    mx_mutex_enter(&glbmutex);
    ret += mx_carray_count(&ddevbase);
    mx_carray_foreach(&drvbase, n, {
        mx_driver_pt pdrv = container(n, mx_driver_t, node);
        ret += mx_carray_count(&pdrv->devbase);
    });
    ret += mx_carray_count(&ndevbase);
    mx_mutex_leave(&glbmutex);

    return ret;
}

/**
 * get the device by name
 * @param name: device name
 * @return: matched device (mx_device_pt)
 */
mx_device_pt mx_device_get (mx_string_pt name)
{
    mx_device_pt ret = mx_null;

    mx_mutex_enter(&glbmutex);

    /* from direct device chain */
    mx_carray_foreach(&ddevbase, n, {
        mx_device_pt pdev = container(n, mx_device_t, node);
        if (mx_strcmp(name, pdev->data->name) == 0)
        {
            ret = pdev;
            goto __mx_device_get_exit;
        }
    });

    /* from drivers */
    mx_carray_foreach(&drvbase, ndrv, {
        mx_driver_pt pdrv = container(ndrv, mx_driver_t, node);
        mx_carray_foreach(&pdrv->devbase, ndev, {
            mx_device_pt pdev = container(ndev, mx_device_t, node);
            if (mx_strcmp(name, pdev->data->name) == 0)
            {
                ret = pdev;
                goto __mx_device_get_exit;
            }
        });
    });

    /* from not matched device chain */
    mx_carray_foreach(&ndevbase, n, {
        mx_device_pt pdev = container(n, mx_device_t, node);
        if (mx_strcmp(name, pdev->data->name) == 0)
        {
            ret = pdev;
            goto __mx_device_get_exit;
        }
    });

__mx_device_get_exit:
    mx_mutex_leave(&glbmutex);

    return ret;
}

/**
 * open a device
 * @param pdev: the descriptor of device
 * @return: the result of open operation (mx_err_t)
 */
mx_err_t mx_device_open (mx_device_pt pdev)
{
    mx_err_t ret = EOK;

    mx_bool_t res = mx_mutex_tryenter(&pdev->mutex);
    if (res && !pdev->isopen)
    {
        if (pdev->isdirect)
        {
            ret = pdev->rely.ops->open(pdev);
        }
        else
        {
            /* check whether the device is linked up */
            if (pdev->rely.drv != mx_null)
            {
                ret = ((mx_driver_pt)pdev->rely.drv)->ops->open(pdev);
            }
            else
            {
                ret = EDEV_NOTMATCHED;
            }
        }
        if (ret == EOK)
        {
            pdev->isopen = mx_true;
        }
    }
    else
    {
        ret = EDEV_BUSY;
    }
    mx_mutex_leave(&pdev->mutex);
    return ret;
}

/**
 * close a device
 * @param pdev: the descriptor of device
 * @return: void
 */
void mx_device_close (mx_device_pt pdev)
{
    mx_bool_t res = mx_mutex_tryenter(&pdev->mutex);
    if (res && pdev->isopen && pdev->oc == 0)
    {
        /* no need to check the pdev->drv is null or not */
        if (pdev->isdirect)
        {
            pdev->rely.ops->close(pdev);
        }
        else
        {
            ((mx_driver_pt)pdev->rely.drv)->ops->close(pdev);
        }
        pdev->isopen = mx_false;
    }
    mx_mutex_leave(&pdev->mutex);
}

/**
 * read data from device
 * @param pdev: the descriptor of device
 * @param buf: a buffer for readed data
 * @param size: buffer size
 * @return: the count of readed data (mx_size_t)
 */
mx_size_t mx_device_read (mx_device_pt pdev, mx_void_pt buf, mx_size_t size)
{
    mx_size_t ret = 0;

    mx_bool_t res = mx_mutex_tryenter(&pdev->mutex);
    if (res && pdev->isopen && pdev->oc != MX_USIZE_MAX)
    {
        pdev->oc ++;
        mx_mutex_leave(&pdev->mutex);
        if (pdev->isdirect)
        {
            ret = pdev->rely.ops->read(pdev, buf, size);
        }
        else
        {
            ret = ((mx_driver_pt)pdev->rely.drv)->ops->read(pdev, buf, size);
        }
        mx_mutex_enter(&pdev->mutex);
        pdev->oc --;
        mx_mutex_leave(&pdev->mutex);
    }
    else
    {
        mx_mutex_leave(&pdev->mutex);
    }
    return ret;
}

/**
 * write data to a device
 * @param pdev: the descriptor of device
 * @param data: target data
 * @param size: the size of target data
 * @return: the count of writed data (mx_size_t)
 */
mx_size_t mx_device_write (mx_device_pt pdev, mx_void_pt data, mx_size_t size)
{
    mx_size_t ret = 0;

    mx_bool_t res = mx_mutex_tryenter(&pdev->mutex);
    if (res && pdev->isopen && pdev->oc != MX_USIZE_MAX)
    {
        pdev->oc ++;
        mx_mutex_leave(&pdev->mutex);
        if (pdev->isdirect)
        {
            ret = pdev->rely.ops->write(pdev, data, size);
        }
        else
        {
            ret = ((mx_driver_pt)pdev->rely.drv)->ops->write(pdev, data, size);
        }
        mx_mutex_enter(&pdev->mutex);
        pdev->oc --;
        mx_mutex_leave(&pdev->mutex);
    }
    else
    {
        mx_mutex_leave(&pdev->mutex);
    }
    return ret;
}

/**
 * control a device
 * @param pdev: the descriptor of device
 * @param ctrl: the control option
 * @param arg: the argument of control option
 * @return: the result of control operation (mx_err_t)
 */
mx_err_t mx_device_ctrl (mx_device_pt pdev, mx_size_t ctrl, mx_void_pt arg)
{
    mx_err_t ret;
    mx_bool_t res = mx_mutex_tryenter(&pdev->mutex);
    if (res)
    {
        if (pdev->isopen)
        {
            if (pdev->isdirect)
            {
                ret = pdev->rely.ops->ctrl(pdev, ctrl, arg);
            }
            else
            {
                ret = ((mx_driver_pt)pdev->rely.drv)->ops->ctrl(pdev, ctrl, arg);
            }
        }
        else
        {
            ret = EDEV_NOTOPEN;
        }
        mx_mutex_leave(&pdev->mutex);
    }
    else
    {
        ret = EDEV_BUSY;
    }
    return ret;
}

static void device_init ()
{
    mx_carray_init(&ndevbase);
    mx_carray_init(&ddevbase);
}
MX_BOARDINIT_EXPORT(device_init, 0);
